/*
 * @(#) $Header$
 *
 * Copyright (C) 2012  Daniel Léonard
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package ca.forklabs.android.gpsexact;

import java.util.List;
import java.util.Locale;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.text.format.DateUtils;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.TextView;
import android.widget.Toast;
import ca.forklabs.android.location.LocationAdapter;

/**
 * Class {@code GPSExactActivity} is the entry point and main activity of the
 * <em>GPSExact</em> application.
 *
 * @author   <a href="mailto:forklabs at gmail.com?subject=ca.forklabs.android.gpsexact.GPSExactActivity">Daniel Léonard</a>
 */
public class GPSExactActivity extends Activity {

//---------------------------
// Instance variables
//---------------------------

   /** The gps listener. */
   private LocationListener gps_listener = new LocationAdapter() {
      @Override
      public void onLocationChanged(Location location) {
         GPSExactActivity.this.display(location);
         }

      @Override
      public void onProviderDisabled(String provider) {
         GPSExactActivity.this.showSorryToast();
         }

      @Override
      public void onProviderEnabled(String provider) {
         GPSExactActivity.this.showLookingForGPSDataToast();
         }
      };

   /** The last known location. */
   private Location last_known_location = null;

   /** Prototype for the intent to launch a map activity. */
   private Intent map_activity_intent = new Intent(Intent.ACTION_VIEW, Uri.parse("geo:0.00,0.00?z=4")); //$NON-NLS-1$
   /** Prototype for the intent to launch a share activity. */
   private Intent share_activity_intent = new Intent(Intent.ACTION_SEND);
   {
      String geo_uri = "https://maps.google.com/maps?q=%0.00N+0.00W&z=4"; //$NON-NLS-1$
      this.share_activity_intent.putExtra(Intent.EXTRA_TEXT, geo_uri);
      this.share_activity_intent.setType("text/plain"); //$NON-NLS-1$
   }


//---------------------------
// Constructors
//---------------------------

   /**
    * Constructor.
    */
   public GPSExactActivity() {
   // nothing
      }


//---------------------------
// Accessors and mutators
//---------------------------

   /**
    * Gets the gps listener.
    * @return   the listener.
    */
   protected LocationListener getGPSListener() {
      return this.gps_listener;
      }

   /**
    * Gets the text view that displays the latitude in degrees-minutes-seconds.
    * @return   the text view.
    */
   protected TextView getLatitudeInDegreesTextView() {
      TextView tv = (TextView) this.findViewById(R.id.latitude_degrees);
      return tv;
      }

   /**
    * Gets the text view that displays the longitude in degrees-minutes-seconds.
    * @return   the text view.
    */
   protected TextView getLongitudeInDegreesTextView() {
      TextView tv = (TextView) this.findViewById(R.id.longitude_degrees);
      return tv;
      }

   /**
    * Gets the text view that displays the latitude in decimal degrees.
    * @return   the text view.
    */
   protected TextView getLatitudeInDecimalTextView() {
      TextView tv = (TextView) this.findViewById(R.id.latitude_decimal);
      return tv;
      }

   /**
    * Gets the text view that displays the longitude in decimal.
    * @return   the text view.
    */
   protected TextView getLongitudeInDecimalTextView() {
      TextView tv = (TextView) this.findViewById(R.id.longitude_decimal);
      return tv;
      }

   /**
    * Gets the text view that displays the altitude.
    * @return   the text view.
    */
   protected TextView getAltitudeTextView() {
      TextView tv = (TextView) this.findViewById(R.id.altitude);
      return tv;
      }

   /**
    * Gets the text view that displays the date and the time.
    * @return   the text view.
    */
   protected TextView getDateTimeTextView() {
      TextView tv = (TextView) this.findViewById(R.id.date_time);
      return tv;
      }

   /**
    * Gets the last known location.
    * @return   the location.
    */
   protected Location getLastKnownLocation() {
      return this.last_known_location;
      }

   /**
    * Changes the last known location.
    * @param   location   the new location.
    */
   protected void setLastKnownLocation(Location location) {
      this.last_known_location = location;
      }

   /**
    * Gets the prototype intent for the map activity.
    * @return   the prototype intent.
    */
   protected Intent getMapActivityIntentPrototype() {
      return this.map_activity_intent;
      }

   /**
    * Gets the prototype intent for the share activity.
    * @return   the prototype intent.
    */
   protected Intent getShareActivityIntentPrototype() {
      return this.share_activity_intent;
      }


//---------------------------
// Instance methods
//---------------------------

   /**
    * Determines if the longitude is eastern or western.
    * @param   longitude   the longitude.
    * @return   {@code "E"} if the value represents a eastern longitude,
    *           {@code "W"} if the value represents a western longitude.
    */
   protected CharSequence EorW(double longitude) {
      CharSequence ew;
      if (0 >= longitude) {
         ew = this.getText(R.string.w);
         }
      else {
         ew = this.getText(R.string.e);
         }

      return ew;
      }

   /**
    * Determines if the latitude is north or south.
    * @param   latitude   the latitude.
    * @return   {@code "N"} if the value represents a northern latitude,
    *           {@code "S"} if the value represents a southern latitude.
    */
   protected CharSequence NorS(double latitude) {
      CharSequence ns;
      if (0 >= latitude) {
         ns = this.getText(R.string.s);
         }
      else {
         ns = this.getText(R.string.n);
         }
      return ns;
      }

   /**
    * Converts a decimal value into degrees-minutes-seconds.
    * @param   value   the value.
    * @return   the degrees-minutes-seconds.
    */
   protected double[] degreesMinutesSeconds(double value) {
      double degrees, minutes, seconds;

      value = Math.abs(value);

      degrees = Math.floor(value);
      value = value - degrees;

      value = value * 60.0;
      minutes = Math.floor(value);
      value = value - minutes;

      seconds = value * 60.0;

      double[] values = new double[] {
         degrees, minutes, seconds,
         };
      return values;
      }

   /**
    * Displays in degree-minute-seconds the latitude and longitude into the
    * correct text views.
    * @param   location   the GPS data point.
    */
   @SuppressWarnings("boxing")
   protected void displayLatLongInDegrees(Location location) {
      String template  = "%2.0f° %2.0f' %2.2f\" %s"; //$NON-NLS-1$

      double latitude = location.getLatitude();
      double longitude = location.getLongitude();

      CharSequence ns = this.NorS(latitude);
      CharSequence ew = this.EorW(longitude);

      double[] values;
      String text;
      TextView tv;

      values = this.degreesMinutesSeconds(latitude);
      text = String.format(template, values[0], values[1], values[2], ns);
      tv = this.getLatitudeInDegreesTextView();
      tv.setText(text);

      values = this.degreesMinutesSeconds(longitude);
      text = String.format(template, values[0], values[1], values[2], ew);
      tv = this.getLongitudeInDegreesTextView();
      tv.setText(text);
      }

   /**
    * Displays in decimal the latitude and longitude into the correct text
    * views.
    * @param   location   the GPS data point.
    */
   @SuppressWarnings("boxing")
   protected void displayLatLongInDecimal(Location location) {
      String template  = "%.6f %s"; //$NON-NLS-1$

      double latitude = location.getLatitude();
      double longitude = location.getLongitude();

      CharSequence ns = this.NorS(latitude);
      latitude = Math.abs(latitude);

      CharSequence ew = this.EorW(longitude);
      longitude = Math.abs(longitude);

      String text;
      TextView tv;

      text = String.format(template, latitude, ns);
      tv = this.getLatitudeInDecimalTextView();
      tv.setText(text);

      text = String.format(template, longitude, ew);
      tv = this.getLongitudeInDecimalTextView();
      tv.setText(text);
      }

   /**
    * Displays the altitude into the correct text view.
    * @param   location   the GPS data point.
    */
   @SuppressWarnings("boxing")
   protected void displayAltitude(Location location) {
      String template = "%.2f m"; //$NON-NLS-1$
      double altitude = location.getAltitude();
      String text = String.format(template, altitude);

      TextView tv = this.getAltitudeTextView();
      tv.setText(text);
      }

   /**
    * Displays the date and time into the correct text view.
    * @param   location   the GPS data point.
    */
   protected void displayDateTime(Location location) {
// TODO : find a better way to display the date and the time
      Context context = this;
      long millis = location.getTime();
      int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME;
      String text = DateUtils.formatDateTime(context, millis, flags);

      TextView tv = this.getDateTimeTextView();
      tv.setText(text);
      }

   /**
    * Displays the content of the GPS data point into the correct text views.
    * @param   location   the GPS data point.
    */
   protected void display(Location location) {
   // always remember, even if null
      this.setLastKnownLocation(location);
   // display if not null
      if (null != location) {
         this.displayLatLongInDegrees(location);
         this.displayLatLongInDecimal(location);
         this.displayAltitude(location);
         this.displayDateTime(location);
         }
      }

   /**
    * Shows a long toast.
    * @param   resId   the id of the text to show.
    */
   protected void showLongToast(int resId) {
      CharSequence text = this.getText(resId);
      int duration = Toast.LENGTH_LONG;
      Toast toast = Toast.makeText(this, text, duration);
      toast.show();
      }

   /**
    * Shows a toast that indicates that the GPS is not enabled and that the
    * application is useless.
    */
   protected void showSorryToast() {
      int resId = R.string.toast_bad;
      this.showLongToast(resId);
      }

   /**
    * Shows a toast that indicates that the application is looking for the GPS
    * signal.
    */
   protected void showLookingForGPSDataToast() {
      int resId = R.string.toast_good;
      this.showLongToast(resId);
      }

   /**
    * Gets the location manager.
    * @return   the location manager.
    */
   protected LocationManager getLocationManager() {
      LocationManager location_manager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
      return location_manager;
      }

   /**
    * Starts the GPS listener.
    */
   protected void startGPSListener() {
      LocationManager location_manager = this.getLocationManager();

      long delays_in_millis = 0L;
      float distance_in_meters = 0.0f;

      LocationListener listener = this.getGPSListener();

      location_manager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
                                              delays_in_millis,
                                              distance_in_meters,
                                              listener);
      }

   /**
    * Stops the GPS listener.
    */
   protected void stopGPSListener() {
      LocationManager location_manager = this.getLocationManager();
      LocationListener listener = this.getGPSListener();
      location_manager.removeUpdates(listener);
      }

   /**
    * Asks for the GPS to be enabled.
    */
   protected void askForGPS() {
      CharSequence dialog_text = this.getText(R.string.gps_dialog_text);
      CharSequence dialog_yes = this.getText(R.string.gps_dialog_yes);
      CharSequence dialog_no = this.getText(R.string.gps_dialog_no);

      new AlertDialog.Builder(this)
            .setMessage(dialog_text)
            .setCancelable(false)
            .setPositiveButton(dialog_yes, new DialogInterface.OnClickListener() {
               @Override
               public void onClick(final DialogInterface dialog, final int id) {
                  String action = Settings.ACTION_LOCATION_SOURCE_SETTINGS;
                  Intent intent = new Intent(action);
                  GPSExactActivity.this.startActivity(intent);
                  }
               })
            .setNegativeButton(dialog_no, new DialogInterface.OnClickListener() {
               @Override
               public void onClick(final DialogInterface dialog, final int id) {
                  dialog.cancel();
                  GPSExactActivity.this.showSorryToast();
                  }
               })
            .create().show();
      }

   /**
    * Formats using {@link Locale#US}.
    * @param   pattern   the format pattern.
    * @param   args   the format arguments.
    * @return   the formatted text.
    */
   protected String format(String pattern, Object... args) {
      String text;

      Locale locale = Locale.getDefault();
      Locale.setDefault(Locale.US);
      try {
         text = String.format(pattern, args);
         }
      finally {
         Locale.setDefault(locale);
         }

      return text;
      }

   /**
    * Starts a map activity with the last known coordinates.
    */
   @SuppressWarnings("boxing")
   protected void startMapActivity() {
      Location location = this.getLastKnownLocation();

      if (null != location) {
         String format = "geo:%.6f,%.6f?z=%d"; //$NON-NLS-1$
         double latitude = location.getLatitude();
         double longitude = location.getLongitude();
         int zoom = 17;
         String geo_uri = this.format(format, latitude, longitude, zoom);

         Uri uri = Uri.parse(geo_uri);

         Intent prototype = this.getMapActivityIntentPrototype();
         Intent intent = prototype.cloneFilter();
         intent.setData(uri);

         this.startActivity(intent);
         }
      else {
         int resId = R.string.toast_no_location;
         this.showLongToast(resId);
         }
      }

   /**
    * Starts a share activity with the last known coordinates.
    */
   @SuppressWarnings("boxing")
   protected void startShareActivity() {
// BUG: find a way for it to work - contact Google+ mobile team?
      Location location = this.getLastKnownLocation();

      if (null != location) {
         double latitude = location.getLatitude();
         double longitude = location.getLongitude();

      // this is what I really want, to enter as the location on Google+
//         String format = "geo:%.6f,%.6f"; //$NON-NLS-1$
//         String geo_uri = String.format(format, latitude, longitude);
//         String action = Intent.ACTION_SEND;
//         Uri uri = Uri.parse(geo_uri);
//         Intent intent = new Intent(action, uri);

         int zoom = 15;
         String format = "https://maps.google.com/maps?q=%.6f+%.6f&z=%d"; //$NON-NLS-1$
         String geo_uri = this.format(format, latitude, longitude, zoom);

         Intent prototype = this.getShareActivityIntentPrototype();
         Intent intent = prototype.cloneFilter();
         intent.putExtra(Intent.EXTRA_TEXT, geo_uri);

         this.startActivity(intent);
         }
      else {
         int resId = R.string.toast_no_location;
         this.showLongToast(resId);
         }
      }


//---------------------------
// Overridden methods from android.app.Activity
//---------------------------

   /**
    * Called back method when any menu item is activated.
    * @param   item   the activated item.
    * @return   {@code true} if the event has been acted upon,
    *           {@code false} otherwise.
    */
   @Override
   public boolean onOptionsItemSelected (MenuItem item) {
      boolean consume = false;

      int id = item.getItemId();
      switch (id) {
         case R.id.menu_google_maps:
            this.startMapActivity();
            consume = true;
            break;

         case R.id.menu_google_plus:
            this.startShareActivity();
            consume = true;
            break;

         default:
            consume = super.onOptionsItemSelected(item);
            break;
         }

      return consume;
      }

   /**
    * Checks to see if an activity can be launched with the given intent. If it
    * is possible, lauch it. Otherwise show a toast that no activity has been
    * found.
    * @param   intent   the intent to launch the activity.
    */
   @Override
   public void startActivity(Intent intent) {
      boolean an_app_exists = this.doesAnAppExistFor(intent);
      if (an_app_exists) {
         super.startActivity(intent);
         }
      else {
         this.showLongToast(R.string.cannot_start_activity);
         }
      }

   /**
    * Determines if an app is available for the given intent if it were to start
    * another activity.
    * @param   intent   the intent.
    * @return   {@code true} if there exists at least on app that can catch the
    *           intent, {@code false} otherwise.
    */
   protected boolean doesAnAppExistFor(Intent intent) {
      PackageManager package_manager = this.getPackageManager();
      List<ResolveInfo> activities = package_manager.queryIntentActivities(intent, 0);
      boolean an_app_exists = false == activities.isEmpty();
      return an_app_exists;
      }

   /**
    * Creates the menu.
    * @return   always {@code true}.
    */
   @Override
   public boolean onCreateOptionsMenu(Menu menu) {
      super.onCreateOptionsMenu(menu);

   // let us inflate the whole menu
      MenuInflater inflater = this.getMenuInflater();
      inflater.inflate(R.menu.actions, menu);

   // and remove items for which there is no app
      Intent map_intent = this.getMapActivityIntentPrototype();
      Intent share_intent = this.getShareActivityIntentPrototype();

      boolean an_app_exists_for_maps = this.doesAnAppExistFor(map_intent);
      boolean an_app_exists_for_share = this.doesAnAppExistFor(share_intent);

      if (false == an_app_exists_for_maps) {
         menu.removeItem(R.id.menu_google_maps);
         }
      if (false == an_app_exists_for_share) {
         menu.removeItem(R.id.menu_google_plus);
         }

      return true;
      }

   /**
    * Lays out the user interface components, shows the last known location if
    * possible and asks for the GPS to be activated if necessary.
    */
   @Override
   public void onCreate(Bundle saved_instance_state) {
      super.onCreate(saved_instance_state);

      this.setContentView(R.layout.gpsexact);

      LocationManager location_manager = this.getLocationManager();

      boolean is_gps_activated = location_manager.isProviderEnabled(LocationManager.GPS_PROVIDER);
      if (is_gps_activated) {
         Location location = location_manager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
         this.display(location);
         }
      else {
         this.askForGPS();
         }
      }

   /**
    * Starts the GPS listener.
    */
   @Override
   protected void onResume() {
      super.onResume();
      this.startGPSListener();
      }

   /**
    * Stops the GPS listener.
    */
   @Override
   protected void onPause() {
      this.stopGPSListener();
      super.onPause();
      }

   }
